iT邦幫忙

2024 iThome 鐵人賽

DAY 22
0
Modern Web

React 學得動嗎系列 第 22

[Day 22] Gym Pro:課程管理系統的實作與日曆視圖整合

  • 分享至 

  • xImage
  •  

今天,我們要來實作健身房管理系統中另一個重要的功能:課程管理。我們來建立課程列表頁面、課程詳情頁面,以及一個日曆來顯示課程安排。

1. 安裝必要的套件

首先,我們需要安裝一些與日曆和表單處理相關的套件:

npm install react-big-calendar date-fns
npm install -D @types/react-big-calendar

2. 課程列表頁面

我們將首先建立課程列表頁面,並利用 TanStack Table 來提供排序和分頁功能,以提升列表的易讀性和可操作性。

src/pages/Classes.tsx 中:

import React from 'react';
import { useQuery } from '@tanstack/react-query';
import { useTable, useSortBy, usePagination } from '@tanstack/react-table';
import axios from 'axios';
import { Button } from "@/components/ui/button"
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"

interface Class {
  id: number;
  name: string;
  instructor: string;
  date: string;
  time: string;
  duration: number;
}

const fetchClasses = async (): Promise<Class[]> => {
  const { data } = await axios.get<Class[]>('/api/classes');
  return data;
};

const Classes: React.FC = () => {
  const { data: classes, isLoading, error } = useQuery(['classes'], fetchClasses);

  const columns = React.useMemo(
    () => [
      {
        Header: '課程名稱',
        accessor: 'name',
      },
      {
        Header: '教練',
        accessor: 'instructor',
      },
      {
        Header: '日期',
        accessor: 'date',
      },
      {
        Header: '時間',
        accessor: 'time',
      },
      {
        Header: '時長 (分鐘)',
        accessor: 'duration',
      },
    ],
    []
  );

  const {
    getTableProps,
    getTableBodyProps,
    headerGroups,
    page,
    nextPage,
    previousPage,
    canNextPage,
    canPreviousPage,
    prepareRow,
  } = useTable(
    { columns, data: classes || [] },
    useSortBy,
    usePagination
  );

  if (isLoading) return <div>載入中...</div>;
  if (error) return <div>發生錯誤:{(error as Error).message}</div>;

  return (
    <div>
      <h1 className="text-2xl font-bold mb-4">課程列表</h1>
      <Table {...getTableProps()}>
        <TableHeader>
          {headerGroups.map(headerGroup => (
            <TableRow {...headerGroup.getHeaderGroupProps()}>
              {headerGroup.headers.map(column => (
                <TableHead {...column.getHeaderProps(column.getSortByToggleProps())}>
                  {column.render('Header')}
                  <span>
                    {column.isSorted
                      ? column.isSortedDesc
                        ? ' 🔽'
                        : ' 🔼'
                      : ''}
                  </span>
                </TableHead>
              ))}
            </TableRow>
          ))}
        </TableHeader>
        <TableBody {...getTableBodyProps()}>
          {page.map(row => {
            prepareRow(row);
            return (
              <TableRow {...row.getRowProps()}>
                {row.cells.map(cell => (
                  <TableCell {...cell.getCellProps()}>{cell.render('Cell')}</TableCell>
                ))}
              </TableRow>
            );
          })}
        </TableBody>
      </Table>
      <div className="mt-4">
        <Button onClick={() => previousPage()} disabled={!canPreviousPage}>
          上一頁
        </Button>
        <Button onClick={() => nextPage()} disabled={!canNextPage}>
          下一頁
        </Button>
      </div>
    </div>
  );
};

export default Classes;

3. 課程日曆

接著,我們將建立一個日曆視圖,讓管理員能夠直觀地查看每周或每月的課程安排。我們將使用 React Big Calendar 來完成這個功能。

src/pages/ClassCalendar.tsx 中:

import React from 'react';
import { Calendar, momentLocalizer } from 'react-big-calendar';
import moment from 'moment';
import 'react-big-calendar/lib/css/react-big-calendar.css';
import { useQuery } from '@tanstack/react-query';
import axios from 'axios';

moment.locale('zh-TW');
const localizer = momentLocalizer(moment);

interface Class {
  id: number;
  title: string;
  start: Date;
  end: Date;
  instructor: string;
}

const fetchClasses = async (): Promise<Class[]> => {
  const { data } = await axios.get<Class[]>('/api/classes');
  return data.map(cls => ({
    ...cls,
    start: new Date(cls.start),
    end: new Date(cls.end),
  }));
};

const ClassCalendar: React.FC = () => {
  const { data: classes, isLoading, error } = useQuery(['classes'], fetchClasses);

  if (isLoading) return <div>載入中...</div>;
  if (error) return <div>發生錯誤:{(error as Error).message}</div>;

  return (
    <div style={{ height: '500px' }}>
      <Calendar
        localizer={localizer}
        events={classes}
        startAccessor="start"
        endAccessor="end"
        style={{ height: '100%' }}
        messages={{
          next: "下一個",
          previous: "上一個",
          today: "今天",
          month: "月",
          week: "週",
          day: "日"
        }}
      />
    </div>
  );
};

export default ClassCalendar;

4. 課程詳情和編輯頁面

接下來,我們將建立一個課程詳情和編輯頁面,讓管理員可以查看和修改課程資訊。

src/pages/ClassDetail.tsx 中:

import React from 'react';
import { useParams } from 'react-router-dom';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { useForm } from 'react-hook-form';
import axios from 'axios';
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Card, CardHeader, CardTitle, CardContent, CardFooter } from "@/components/ui/card"

interface Class {
  id: number;
  name: string;
  instructor: string;
  date: string;
  time: string;
  duration: number;
}

const fetchClass = async (id: string): Promise<Class> => {
  const { data } = await axios.get<Class>(`/api/classes/${id}`);
  return data;
};

const updateClass = async (classData: Class): Promise<Class> => {
  const { data } = await axios.put<Class>(`/api/classes/${classData.id}`, classData);
  return data;
};

const ClassDetail: React.FC = () => {
  const { id } = useParams<{ id: string }>();
  const queryClient = useQueryClient();
  const { data: classData, isLoading, error } = useQuery(['class', id], () => fetchClass(id!));

  const { register, handleSubmit, formState: { errors } } = useForm<Class>();

  const mutation = useMutation(updateClass, {
    onSuccess: () => {
      queryClient.invalidateQueries(['class', id]);
    },
  });

  const onSubmit = (data: Class) => {
    mutation.mutate(data);
  };

  if (isLoading) return <div>載入中...</div>;
  if (error) return <div>發生錯誤:{(error as Error).message}</div>;

  return (
    <Card>
      <CardHeader>
        <CardTitle>課程詳情</CardTitle>
      </CardHeader>
      <CardContent>
        <form onSubmit={handleSubmit(onSubmit)}>
          <div className="space-y-4">
            <Input {...register('name', { required: '請輸入課程名稱' })} defaultValue={classData?.name} placeholder="課程名稱" />
            {errors.name && <p className="text-red-500">{errors.name.message}</p>}

            <Input {...register('instructor', { required: '請輸入教練姓名' })} defaultValue={classData?.instructor} placeholder="教練" />
            {errors.instructor && <p className="text-red-500">{errors.instructor.message}</p>}

            <Input {...register('date')} defaultValue={classData?.date} placeholder="日期" type="date" />
            <Input {...register('time')} defaultValue={classData?.time} placeholder="時間" type="time" />
            <Input {...register('duration', { required: '請輸入課程時長', min: 1 })} defaultValue={classData?.duration} placeholder="時長 (分鐘)" type="number" />
            {errors.duration && <p className="text-red-500">{errors.duration.message}</p>}
          </div>
          <Button type="submit" className="mt-4">更新課程資料</Button>
        </form>
      </CardContent>
    </Card>
  );
};

export default ClassDetail;

5. 更新路由

最後,我們將這些新頁面整合進路由系統。在 src/App.tsx 中進行相應更新:

import React from 'react';
import { BrowserRouter as Router, Route, Routes } from 'react-router-dom';
import MainLayout from './layouts/MainLayout';
import Dashboard from './pages/Dashboard';
import Members from './pages/Members';
import MemberDetail from './pages/MemberDetail';
import Classes from './pages/Classes';
import ClassCalendar from './pages/ClassCalendar';
import ClassDetail from './pages/ClassDetail';
import Login from './pages/Login';

function App() {
  return (
    <Router>
      <Routes>
        <Route path="/login" element={<Login />} />
        <Route path="/" element={<MainLayout />}>
          <Route index element={<Dashboard />} />
          <Route path="members" element={<Members />} />
          <Route path="members/:id" element={<MemberDetail />} />
          <Route path="classes" element={<Classes />} />
          <Route path="class-calendar" element={<ClassCalendar />} />
          <Route path="classes/:id" element={<ClassDetail />} />
        </Route>
      </Routes>
    </Router>
  );
}

export default App;

小結

今天完成了 Gym Pro 系統中的課程管理功能,包括:

  1. 建立了支持排序與分頁的課程列表頁面。
  2. 使用 React Big Calendar 實現了直觀的課程日曆。
  3. 設計了課程詳情與編輯頁面,並使用 React Hook Form 進行表單處理與驗證。
  4. 使用 React Query 的 useMutation 實現了課程資料的更新功能。

接下來,我們將繼續開發更多實用功能,提升 Gym Pro 的可用性與用戶體驗。


上一篇
[Day 21] Gym Pro:實作會員管理功能
下一篇
[Day 23] Gym Pro:打造報表和分析功能
系列文
React 學得動嗎30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言